package com.nightpiter.view;

import static com.nightpiter.controller.log.Logger.log;
import static com.nightpiter.controller.log.Logger.logProfilerBegin;
import static com.nightpiter.controller.log.Logger.logProfilerEnd;

import java.util.LinkedList;
import java.util.Map;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Point;
import android.graphics.PointF;
import android.graphics.Rect;
import android.os.Handler;
import android.util.AttributeSet;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;

import com.nightpiter.model.MetroModel;
import com.nightpiter.model.objects.Station;
import com.nightpiter.model.objects.Transition;
import com.nightpiter.view.drawer.MetroDrawer;
import com.nightpiter.view.drawer.advanced.AdvancedMetroDrawer;

/**
 * MetroScreen class. Respond for drawing the map, managing the cache and so on.
 * 
 * @author Eldar Abusalimov (eldar.abusalimov@gmail.com)
 */
public class MetroScreen extends View {
	/**
	 * the subscriber that would be notified about most of events
	 */
	private Subscriber subscriber;

	/**
	 * The drawer used to analyze model and draw it's representation to the
	 * canvas
	 */
	private MetroDrawer drawer;

	/**
	 * Drawing actions available to perform by this View
	 * 
	 * @author Eldar Abusalimov (eldar.abusalimov@gmail.com)
	 */
	public enum Action {
		DRAW_METRO, SHOW_WAY, HIDE_WAY, TRANSLATE, SCALE, DRAW_SELECTION, NONE, IDLE, LOOP
	}

	/**
	 * the action which should be performed with the next invalidation
	 */
	private Action action = Action.NONE;

	/**
	 * selected station;
	 */
	private Station selectedStation;

	/**
	 * the set of selected transitions;
	 */
	private LinkedList<Transition> selectedTransitions = null;

	/**
	 * new center coordinates
	 */
	private int deltaOffsetX = 0, deltaOffsetY = 0;

	/**
	 * new scale
	 */
	private float deltaScale = 1;

	/**
	 * current center coordinates
	 */
	private int offsetX = 0, offsetY = 0;

	/**
	 * point to smoothly scroll to
	 */
	private int stepX = 0, stepY = 0;

	/**
	 * steps to complete the scrolling
	 */
	private int stepsRemain = 0;

	/**
	 * total steps to smooth scroll
	 */
	private static final int TOTAL_STEPS = 7;

	/**
	 * current scale value
	 */
	private float scale = 1;

	/**
	 * needed scale to fit map onto the screen
	 */
	private float fitScale = 1;

	/**
	 * cache bitmap
	 */
	private Bitmap currentCache;

	/**
	 * canvas to draw cache
	 */
	private Canvas cacheCanvas;

	/**
	 * cache dimensions multiplier
	 */
	private static final int CACHE_DIM = 3;

	/**
	 * view dimensions
	 */
	private int width, height;

	/**
	 * view rectangle
	 */
	private Rect screenRect;

	/**
	 * view rectangle
	 */
	private Rect defaultRect;

	/**
	 * @param context
	 */
	public MetroScreen(Context context) {
		super(context);
	}

	/**
	 * @param context
	 * @param attrs
	 * @param inflateParams
	 */
	@SuppressWarnings("unchecked")
	public MetroScreen(Context context, AttributeSet attrs, Map inflateParams) {
		super(context, attrs, inflateParams);
	}

	/**
	 * @param context
	 * @param attrs
	 * @param inflateParams
	 * @param defStyle
	 */

	@SuppressWarnings("unchecked")
	public MetroScreen(Context context, AttributeSet attrs, Map inflateParams,
			int defStyle) {
		super(context, attrs, inflateParams, defStyle);
	}

	/**
	 * creates new cache bitmap with appropriate dimension
	 */
	@Override
	protected void onSizeChanged(int w, int h, int oldw, int oldh) {
		super.onSizeChanged(w, h, oldw, oldh);
		this.init();
		execute(Action.DRAW_METRO);
	}

	/**
	 * all class initializations
	 */
	void init() {
		this.setFocusable(true);

		int width = getWidth();
		int height = getHeight();
		this.width = width;
		this.height = height;
		this.screenRect = new Rect(0, 0, width, height);
		this.defaultRect = new Rect(width, height, width * 2, height * 2);
		int cacheWidth = width * CACHE_DIM;
		int cacheHeight = height * CACHE_DIM;
		this.currentCache = Bitmap.createBitmap(cacheWidth, cacheHeight,
				Bitmap.Config.RGB_565);

		this.cacheCanvas = new Canvas(currentCache);
		this.cacheCanvas.translate(cacheWidth / 2, cacheHeight / 2);
	}

	/**
	 * set view drawer
	 * 
	 * @param drawer -
	 *            new drawer, implements Drawer
	 */
	void setDrawer(MetroDrawer drawer) {
		this.drawer = drawer;
		log("setting new Drawer: " + this.drawer.getClass().getSimpleName());
	}

	@Override
	protected void onDraw(Canvas canvas) {

		int width = this.width;
		int height = this.height;

		switch (action) {
		case DRAW_METRO:

			this.updateCache();
			canvas.drawBitmap(this.currentCache, new Rect(width, height,
					width * 2, height * 2), this.screenRect, null);
			this.drawSelection(canvas);

			this.action = Action.IDLE;
			break;

		case LOOP:
			if (stepsRemain-- > 0) {
				this.deltaOffsetX += stepX;
				this.deltaOffsetY += stepY;
			} else {
				stepX = 0;
				stepY = 0;
				action = Action.DRAW_METRO;
			}
			invalidate();
			// do not break; enter the cache drawing block
		case DRAW_SELECTION:
		case TRANSLATE:
		case SCALE:

			int scaledCenterX = (int) (width / deltaScale / 2);
			int scaledCenterY = (int) (height / deltaScale / 2);
			int cacheCenterX = width * CACHE_DIM / 2;
			int cacheCenterY = height * CACHE_DIM / 2;

			Rect drawingRect = new Rect(cacheCenterX - scaledCenterX
					- deltaOffsetX,
					cacheCenterY - scaledCenterY - deltaOffsetY, cacheCenterX
							+ scaledCenterX - deltaOffsetX, cacheCenterY
							+ scaledCenterY - deltaOffsetY);
			canvas.drawBitmap(this.currentCache, drawingRect, this.screenRect,
					null);
			this.drawSelection(canvas);
			break;

		case IDLE:
			canvas.drawBitmap(this.currentCache, this.defaultRect,
					this.screenRect, null);
			this.drawSelection(canvas);
			break;

		case NONE:
		default:
			// canvas.drawColor(Color.WHITE);
			break;
		}

	}

	/**
	 * redraw the cached image
	 */
	private void updateCache() {
		this.scale *= this.deltaScale;
		this.offsetX = (int) (this.offsetX * this.deltaScale)
				+ this.deltaOffsetX;
		this.offsetY = (int) (this.offsetY * this.deltaScale)
				+ this.deltaOffsetY;
		this.deltaScale = 1;
		this.deltaOffsetX = 0;
		this.deltaOffsetY = 0;
		if (drawer != null) {
			this.drawer.setScale(scale);
			this.drawer.setOffset(offsetX, offsetY);
			this.drawer.drawMetro(cacheCanvas);
			if (this.selectedTransitions != null) {
				this.drawer.showWay(this.selectedTransitions, cacheCanvas);
			}
		}
	}

	/**
	 * create new MetroDrawer instance and compute image bounds
	 * 
	 * @param model
	 */
	void setModel(MetroModel model) {

		(new DrawerPreparerThread(model, new Handler())).start();

	}

	/**
	 * draw the stationSelection
	 */
	private void drawSelection(Canvas canvas) {
		if (drawer != null) {
			this.drawer.setScale(this.scale * this.deltaScale);
			this.drawer.setOffset((int) (this.offsetX * this.deltaScale)
					+ this.deltaOffsetX, (int) (this.offsetY * this.deltaScale)
					+ this.deltaOffsetY);
			canvas.translate(width / 2, height / 2);
			if (selectedStation != null) {
				this.drawer.selectStation(selectedStation, canvas);
			}
			canvas.translate(-width / 2, -height / 2);
		}
	}

	/**
	 * add a station to the selection
	 * 
	 * @param station
	 *            the station to add
	 * @param scrollToPoint
	 *            enable putting the point onto the center
	 */
	void selectStation(Station station, boolean scrollToPoint) {
		this.selectedStation = station;
		if (scrollToPoint) {
			scrollToPoint(modelToScreen(station.getPosition()), (float) 0.5,
					(float) 0.3);
		} else {
			execute(Action.DRAW_SELECTION);
		}
	}

	/**
	 * remove a station from the selection
	 */
	void deselectStation() {
		this.selectedStation = null;
		execute(Action.DRAW_SELECTION);
	}

	/**
	 * add transitions to the selection
	 * 
	 * @param transitions
	 *            transitions to add
	 */
	void selectTransitions(LinkedList<Transition> transitions) {
		this.selectedTransitions = transitions;
		drawMetro();
	}

	/**
	 * remove transitions from the selection
	 */
	void deselectTransitions() {
		if (this.selectedTransitions != null) {
			this.selectedTransitions = null;
			drawMetro();
		}
	}

	/**
	 * set new scale
	 * 
	 * @param newZoom
	 */
	void setScale(int newZoom) {
		this.deltaScale = (newZoom + fitScale) / this.scale;
		// log("old scale: " + this.scale + ", new scale: " + newZoom
		// + ", new deltaScale: " + deltaScale);
		this.execute(Action.SCALE);
	}

	/**
	 * shift the image
	 * 
	 * @param x
	 * @param y
	 */
	void setOffset(int x, int y) {
		this.deltaOffsetX += x;
		this.deltaOffsetY += y;
		// log("offset by x: " + x + ", by y: " + y + ", new deltaOffsetX: "
		// + deltaOffsetX + ", new deltaOffsetY: " + deltaOffsetY);
		execute(Action.TRANSLATE);
	}

	/**
	 * put a point (x,y) into the screen center
	 * 
	 * @param x
	 * @param y
	 * @param centerX -
	 *            the x multiplier
	 * @param centerY -
	 *            the y multiplier
	 */
	void scrollToPoint(PointF screenPoint, float centerX, float centerY) {
		float realCenterX = width * centerX;
		float realCenterY = height * centerY;
		this.stepX = (int) ((realCenterX - screenPoint.x) / TOTAL_STEPS);
		this.stepY = (int) ((realCenterY - screenPoint.y) / TOTAL_STEPS);
		if (this.stepX != 0 || this.stepY != 0) {
			this.stepsRemain = TOTAL_STEPS;
			execute(Action.LOOP);
		} else {
			execute(Action.DRAW_SELECTION);
		}
	}

	/**
	 * update the cache and render it to screen
	 */
	void drawMetro() {
		execute(Action.DRAW_METRO);
	}

	/**
	 * stimulate the view to redraw and execute given action
	 * 
	 * @param action
	 */
	public void execute(Action action) {
		this.action = action;
		postInvalidate();
	}

	/**
	 * @return the fit scale
	 */
	float getFitScale() {
		return fitScale;
	}

	/**
	 * converts screen coordinates to model
	 * 
	 * @param realX
	 *            the screen X
	 * @param realY
	 *            the screen Y
	 * @return point with model coordinates
	 */
	public PointF screenToModel(int realX, int realY) {
		return new PointF((realX - offsetX - width / 2) / scale, (realY
				- offsetY - height / 2)
				/ scale);
	}

	/**
	 * converts model coordinates to screen
	 * 
	 * @param modelX
	 *            the model X
	 * @param modelY
	 *            the model Y
	 * @return point with screen coordinates
	 */
	public PointF modelToScreen(Point modelPoint) {
		return new PointF(modelPoint.x * scale + offsetX + width / 2,
				modelPoint.y * scale + offsetY + height / 2);
	}

	/**
	 * subscribes to notifications
	 * 
	 * @param subscriber
	 *            the Subscriber
	 */
	public void subscribe(Subscriber subscriber) {
		if (subscriber != null) {
			this.subscriber = subscriber;
		}
	}

	@Override
	public boolean onTouchEvent(MotionEvent event) {
		super.onTouchEvent(event);
		if (subscriber != null) {
			subscriber.onTouchEvent(event);
		}
		return true;

	}

	@Override
	public boolean onKeyDown(int keyCode, KeyEvent event) {
		super.onKeyDown(keyCode, event);
		if (subscriber != null) {
			subscriber.onKeyDown(keyCode, event);
		}
		return true;
	}

	@Override
	public boolean onKeyUp(int keyCode, KeyEvent event) {
		super.onKeyUp(keyCode, event);
		if (subscriber != null) {
			subscriber.onKeyUp(keyCode, event);
		}
		return true;
	}

	/**
	 * Subscriber interface. Should be implemented by any class that would
	 * receive notifications about MetroScreen View event fires
	 * 
	 * @author Eldar Abusalimov (eldar.abusalimov@gmail.com)
	 */
	public interface Subscriber {

		public void onTouchEvent(MotionEvent event);

		public void onKeyDown(int keyCode, KeyEvent event);

		public void onKeyUp(int keyCode, KeyEvent event);

		public void onDrawerPrepareComplete();

	}

	/**
	 * This Thread prepare the model to draw it on the screen
	 * 
	 * @author Eldar Abusalimov (eldar.abusalimov@gmail.com)
	 */
	private class DrawerPreparerThread extends Thread {
		/**
		 * model to prepare
		 */
		private MetroModel model;

		/**
		 * handler from UI thread
		 */
		private Handler handler;

		/**
		 * default constructor
		 */
		public DrawerPreparerThread(MetroModel model, Handler handler) {
			super("DrawerPreparerThread");
			this.model = model;
			this.handler = handler;
		}

		@Override
		public void run() {
			super.run();

			logProfilerBegin("preparing View to work with new model");
			selectedStation = null;

			setDrawer(new AdvancedMetroDrawer(model));

			Rect bounds = drawer.getBounds();
			log("model bounds: width = " + bounds.width() + ", height = "
					+ bounds.height());
			final int MARGIN = 25;
			int width = MetroScreen.this.width;
			int height = MetroScreen.this.height;
			log("view dimensions: width = " + width + ", height = " + height);
			fitScale = Math.min((float) (width - MARGIN) / bounds.width(),
					(float) (height - MARGIN) / bounds.height());

			scale = fitScale;
			offsetX = -(int) ((bounds.left + bounds.right) * fitScale / 2);
			offsetY = -(int) ((bounds.top + bounds.bottom) * fitScale / 2);
			log("new fitScale: " + fitScale);

			logProfilerEnd("prepare View");
			execute(Action.DRAW_METRO);

			handler.post(new Runnable() {
				public void run() {
					subscriber.onDrawerPrepareComplete();
				}
			});
		}

	}

}
